/**
 * Copyright (c) 2009  Collaborative Development Group, C.S. Dept., University of Bari
 *
 * All rights reserved. This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License v1.0  which accompanies this distribution, and is 
 * available at http://www.eclipse.org/legal/epl-v10.html
 */
package it.uniba.di.cdg.penelope.ui.mvp;

import static org.osgi.service.log.LogService.LOG_DEBUG;
import it.uniba.di.cdg.penelope.ui.IPresentationModel;
import it.uniba.di.cdg.penelope.ui.annotation.Presenter;
import it.uniba.di.cdg.penelope.ui.annotation.View;
import it.uniba.di.cdg.penelope.ui.mvp.databinding.IDataBindingService;
import it.uniba.di.cdg.penelope.ui.mvp.scope.ViewScope;
import static it.uniba.di.cdg.penelope.util.ReflectionUtilities.typeOf;
import javax.annotation.PostConstruct;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.osgi.service.log.LogService;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import static it.uniba.di.cdg.penelope.util.ReflectionUtilities.scanAnnotation;

/**
 * Interceptor for performing wiring of the MVP triplet. In particular, performs:
 * <ul>
 * <li>databinding between the presentation model and the view;</li>
 * <li>wiring between the model and the presenter, for model change events using the Beans Framework</li>
 * </ul>
 * <strong>This class is not meant to be used by clients directly</strong>.
 * <br/>
 */
public class MVPWiringPostProcessor implements MethodInterceptor {
	
	private final Provider<?> presentationModelProvider;
	
	private final Provider<?> presenterProvider;
	
	private final String storyId;
	
	@Inject
	private LogService logger;
	
	@Inject
	private IDataBindingService dataBindingService;
	
	@Inject
	private IActionWiringService actionWiringService;

	@Inject
	@Named( "viewScope" )
	public ViewScope viewScope;	
	
	/**
	 * @param modelProvider
	 * @param presenterProvider 
	 */
	public MVPWiringPostProcessor( String storyId, Provider<?> modelProvider, Provider<?> presenterProvider ) {
		super();
		this.storyId = storyId;
		this.presenterProvider = presenterProvider;
		this.presentationModelProvider = modelProvider;
	}

	/* (non-Javadoc)
	 * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
	 */
	@Override
	public Object invoke( MethodInvocation methodInvocation ) throws Throwable {
		if (isViewCreation( methodInvocation )) {
			return wire( methodInvocation );
		} else {
			return unwire( methodInvocation );
		}
	}

	/**
	 * @param methodInvocation
	 * @return
	 * @throws Throwable
	 */
	private Object wire( MethodInvocation methodInvocation ) throws Throwable {
		logger.log( LOG_DEBUG, String.format( "Wiring MVP for story \"%s\"", storyId ) );
		
		// Execute view initialization code.
		Object result = methodInvocation.proceed();
		
		Object v = methodInvocation.getThis(); 
		if (!typeOf( v, IManagedView.class )) {
			throw new MVPWiringException( "[XXX] View should implement IManagedView" );
		}

		// Begin scope
		viewScope.enter( getScopeId( v ) );
		
		Object m = presentationModelProvider.get();
		Object p = presenterProvider.get();
		if (!(p instanceof IPresenter)) {
			throw new MVPWiringException( "[XXX] Presenter should implement IAugmentedPresenter" );
		}
		if (!(m instanceof IPresentationModel)) {
			throw new MVPWiringException( "[XXX] Presentation model should implement IPresentationModel" );
		}

		
		IPresentationModel presentationModel = (IPresentationModel) m;
		IPresenter presenter = (IPresenter) p;
		IManagedView view = (IManagedView) methodInvocation.getThis(); // XXX Damn me!

		// 1. Register the presenter for presentation model change events.
		Presenter presenterAnnotation = presenter.getClass().getAnnotation( Presenter.class );
		if (presenterAnnotation != null) {
			String [] propertyNames = presenterAnnotation.events();
			
			if (propertyNames.length == 0) {
				presentationModel.addPropertyChangeListener( presenter );
			} else {
				for (int i = 0; i < propertyNames.length; i++) {
					presentationModel.addPropertyChangeListener( propertyNames[i], presenter );
				}
			}
		} else {
			throw new MVPWiringException( "Presenter class must be annotated with @Presenter!" );
		}

		// 2. Perform databinding between the model and the view.
		dataBindingService.bind( view, presentationModel );

		// 3. Wire GUI actions to events
		actionWiringService.wire( view, presenter );
		
		// 4. Wire the presenter to view events
		view.registerPresenter( presenter );

		// 5. Notify presenter that view is up and running 
		presenter.onViewCreated();
		
		logger.log( LOG_DEBUG, String.format( "Wiring MVP for story \"%s\": Done!", storyId ) );
		return result;
	}
	
	/**
	 * @param methodInvocation
	 * @return
	 * @throws Throwable 
	 */
	private Object unwire( MethodInvocation methodInvocation ) throws Throwable {
		logger.log( LOG_DEBUG, String.format( "Unwiring MVP for story \"%s\"", storyId ) );

		IPresentationModel presentationModel = (IPresentationModel) presentationModelProvider.get();
		IPresenter presenter = (IPresenter) presenterProvider.get();
		IManagedView view = (IManagedView) methodInvocation.getThis(); // XXX Damn me!
		
		presenter.onViewDisposed();

		view.unregisterPresenter( presenter );
		
		// Execute view initialization code.
		Object result = methodInvocation.proceed();

		// Destroy scope
		viewScope.exit( getScopeId( view ) );
		
		logger.log( LOG_DEBUG, String.format( "Unwiring MVP for story \"%s\": Done!", storyId ) );
		return result;
	}
	
	/**
	 * @param methodInvocation
	 * @return
	 */
	private boolean isViewCreation( MethodInvocation methodInvocation ) {
		return methodInvocation.getMethod().isAnnotationPresent( PostConstruct.class );
	}
	
	private String getScopeId( Object v ) {
		View viewAnnotation = scanAnnotation( v.getClass(), View.class ); 
		if (viewAnnotation == null) {
			throw new MVPWiringException( String.format( "View %s has is not annotated with @View", v.getClass() ) );
		}
		return viewAnnotation.value();
	}
	
}